package com.imooc.filter;

import com.google.gson.Gson;
import com.imooc.base.BaseInfoProperties;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * @version 1.0
 * @Author Mr.Yang
 * @Date 2024-08-09 14:43
 * @question: ip黑名单限制
 */

@Component
@Slf4j
public class IPLimitFilter extends BaseInfoProperties implements GlobalFilter, Ordered {

    @Autowired
    private ExcludeUrlProperties excludeUrlProperties;

    //路径匹配的规则器
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    //ip连续请求的次数
    @Value("${blackIP.continueCounts}")
    private Integer continueCounts;
    //ip判断的时间间隔,单位:秒
    @Value("${blackIP.timeInterval}")
    private Integer timeInterval;
    //限制的时间,单位:秒
    @Value("${blackIP.limitTimes}")
    private Integer limitTimes;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //获取当前url路径
        String path = exchange.getRequest().getURI().getPath();

        //查看是否包含在需要限制的url路径当中
        List<String> ipLimitList = excludeUrlProperties.getIpLimitUrls();
        for (String ipLimit : ipLimitList) {
            //是否能匹配到
            if (antPathMatcher.match(ipLimit, path)) {
                //匹配到了 需要对当前路径进行拦截校验
                log.info("IpLimitFilter - 拦截到需要进行ip限流校验的方法： URL = " + path);
                return doLimit(exchange,chain);
            }
        }

        //没匹配到 正常放行即可
        return chain.filter(exchange);
    }

    /**
     * ip限流校验的方法
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) {

        //获取当前用户ip
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String ip = IPUtil.getIP(serverHttpRequest);

        //若当前ip请求该路径在20秒[timeInterval]内,超过了3[continueCounts]次,则需要限制其30[limitTimes]秒
        //正常的ip
        final String ipRedisKey = "gateway-ip:" + ip;
        //被拦截的黑名单,即已经被关进了小黑屋
        final String ipRedisLimitedKey = "gateway-ip:limit:" + ip;

        //去获取当前ip,查询是否被关进了小黑屋,如果被关进去了还剩多长时间
        long limitLeftTimes = redis.ttl(ipRedisLimitedKey);
        if (limitLeftTimes > 0) {
            //证明还在小黑屋 直接返回错误
            return renderErrorMsg(exchange,ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
        }

        //如果没被关进小黑屋 则开始记录访问的次数
        long requestCounts = redis.increment(ipRedisKey, 1);
        //判断如果是第一次进来 也就是0开始的 则初期访问就是1 需要设置间隔的时间 也就是连续请求的间隔时间
        //即现在设置的间隔时间是20秒 比如该ip用户刚过来设置了20秒间隔过期,在20秒之内又点击了一次,后续没点击,则过了这20秒又需要重新记录
        if (requestCounts == 1) {
            redis.expire(ipRedisKey, timeInterval);
        }

        //走到此处证明在该20秒时间间隔当中可能还有可访问次数,也可能是没了,有的话放行,没有的话抛出错误
        if (requestCounts > continueCounts) {
            //设置黑名单
            redis.set(ipRedisLimitedKey,ipRedisLimitedKey,limitTimes);
            return renderErrorMsg(exchange,ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
        }

        return chain.filter(exchange);
    }


    /**
     * 重新包装并且返回错误信息
     * @param exchange
     * @param statusEnum
     * @return
     */
    private Mono<Void> renderErrorMsg(ServerWebExchange exchange, ResponseStatusEnum statusEnum) {

        //获得response
        ServerHttpResponse response = exchange.getResponse();
        //构建jsonResult
        GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
        //修改response的code码为500(客户端返回的不一定是500 是返回的自己的状态码)
        response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        //设定header类型
        if (response.getHeaders().containsKey("Content-Type"))
            response.getHeaders().add("Content-Type", MimeTypeUtils.APPLICATION_JSON_VALUE);

        //转换json并向response中写入数据
        String resultJson = new Gson().toJson(jsonResult);
        DataBuffer dataBuffer = response.bufferFactory().wrap(resultJson.getBytes(StandardCharsets.UTF_8));

        return response.writeWith(Mono.just(dataBuffer));
    }

    //过滤器的顺序 数字越小优先级则越大
    //先过url的过滤器 如果url不匹配那么就没必要过ipLimit的
    @Override
    public int getOrder() {
        return 1;
    }


}
